package com.leyou.search.service;

import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.client.item.ItemClient;
import com.leyou.common.exception.pojo.ExceptionEnum;
import com.leyou.common.exception.pojo.LyException;
import com.leyou.common.pojo.PageResult;
import com.leyou.common.utils.BeanHelper;
import com.leyou.common.utils.HighlightUtils;
import com.leyou.common.utils.JsonUtils;
import com.leyou.item.dto.SputDTO;
import com.leyou.item.pojo.*;
import com.leyou.search.dto.GoodsDTO;
import com.leyou.search.dto.SearchRequest;
import com.leyou.search.dto.SearchResult;
import com.leyou.search.pojo.Goods;
import com.leyou.search.repository.SearchRepository;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 搜索业务
 */
@Service
public class SearchService {
    @Autowired
    private SearchRepository searchRepository;

    @Autowired
    private ElasticsearchTemplate esTemplate;

    @Autowired
    private ItemClient itemClient;


    /**
     * 从MySQL导入数据到ES的方法
     */
    public void importData(){
        int page = 1;//当前页码
        int rows = 100;//每页大小
        int totalPage=1;//总页数

        //1.分页查询Spu数据
        do {
            PageResult<SputDTO> pageResult = itemClient.spuPageQuery(page, rows, null, true);//注意：只导入上架的商品

            //2.取出Spu列表
            List<SputDTO> sputDTOList = pageResult.getItems();

            if(CollectionUtils.isNotEmpty(sputDTOList)){
                //3.把List<SputDTO>转换List<Goods>
                List<Goods> goodsList = sputDTOList.stream().map(this::buildGoods).collect(Collectors.toList());

               /* List<Goods> goodsList = new ArrayList<>();
                sputDTOList.forEach(sputDTO -> {
                    Goods goods = this.buildGoods(sputDTO);
                    goodsList.add(goods);
                });*/

                //4.保存Goods对象
                searchRepository.saveAll(goodsList);
            }

            totalPage = pageResult.getTotalPage().intValue();
            page++;
        }while (page<=totalPage);

    }

    /**
     * 把一个SpuDTO对象转换为Goods对象的方法
     */
    public Goods buildGoods(SputDTO sputDTO){
        Goods goods = new Goods();

        //一、处理skus属性
        List<Sku> skuList = itemClient.findSkusBySpuId(sputDTO.getId());
        //设计一个List集合存储每个Sku需要的数据
        List<Map<String,Object>> skuMapList = new ArrayList<>();
        //封装需要的Sku属性值
        String all = "";
        Set<Long> price = null;
        if(CollectionUtils.isNotEmpty(skuList)){
            skuList.forEach(sku -> {
                Map<String,Object> skuMap = new HashMap<>();
                skuMap.put("id",sku.getId());
                skuMap.put("images",sku.getImages());
                skuMap.put("price",sku.getPrice());
                skuMapList.add(skuMap);
            });

            //二、处理all属性
            all =
                    sputDTO.getName()
                    +sputDTO.getSubTitle()+
                    skuList.stream().map(Sku::getTitle).collect(Collectors.joining(""));

            //三、处理price属性
            price = skuList.stream().map(Sku::getPrice).collect(Collectors.toSet());
        }

        //四、处理specs属性
        /**
         * specs的格式：
         *   通用的规格参数： <tb_spec_param的name  , tb_spu_detail的generic_spec  >
         *   特有的规格参数： <tb_spec_param的name  , tb_spu_detail的special_spec >
         */
        //1）根据分类查询规格参数（searching=true）
        List<SpecParam> specParams = itemClient.findSpecParams(null, sputDTO.getCid3(), true);

        //2）根据spuId查询SpuDetail
        SpuDetail spuDetail = itemClient.findSpuDetailBySpuId(sputDTO.getId());

        //3）取出SpuDetail的genericSpec 以及 SpuDetail的specialSpec
        String genericSpec = spuDetail.getGenericSpec();
        String specialSpec = spuDetail.getSpecialSpec();

        //4）封装成Map集合
        Map<String,Object> specs = new HashMap<>();

        if(CollectionUtils.isNotEmpty(specParams)){
            specParams.forEach(specParam -> {
                String key = specParam.getName();
                Object value = null;


                if(specParam.getGeneric()){
                    //通用参数
                    //将genericSpec转换为Map集合
                    Map<Long, Object> genericSpecMap = JsonUtils.toMap(genericSpec, Long.class, Object.class);

                    //通过参数ID取出对应的规格参数值
                    value = genericSpecMap.get(specParam.getId());

                }else{
                    //特有参数
                    //将specialSpec转换为Map集合
                    Map<Long, List> specialSpecMap = JsonUtils.nativeRead(specialSpec, new TypeReference<Map<Long, List>>() {});

                    //通过参数ID取出对应的规格参数值
                    value = specialSpecMap.get(specParam.getId());
                }


                //判断该参数是否为数字，如果为数字，则转换为对应的区间值
                if(specParam.getNumeric()){
                    //将数字转换为区间
                    value = chooseSegment(value,specParam);
                }


                specs.put(key,value);
            });
        }


        goods.setId(sputDTO.getId());
        goods.setSpuName(sputDTO.getName());
        goods.setSubTitle(sputDTO.getSubTitle());
        goods.setCreateTime(sputDTO.getCreateTime().getTime());
        goods.setBrandId(sputDTO.getBrandId());
        goods.setCategoryId(sputDTO.getCid3());
        goods.setPrice(price);
        goods.setAll(all);
        goods.setSkus(JsonUtils.toString(skuMapList));
        goods.setSpecs(specs);
        return goods;
    }


    /**
     * 将数子类型的参数转换为区间范围的方法
     * @param value
     * @param p
     * @return
     */
    private String chooseSegment(Object value, SpecParam p) {
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }

    public SearchResult<GoodsDTO> goodsSearchPage(SearchRequest searchRequest) {
        //1.创建SearchResult对象
        SearchResult<GoodsDTO> searchResult = new SearchResult<>();

        //2.封装商品搜索页的全部数据

        //2.1 商品分页列表
        PageResult<GoodsDTO> pageResult =  itemQueryPage(searchRequest);

        //2.2 商品搜索过滤条件
        Map<String,Object> filterConditions = filterConditionsQuery(searchRequest);

        //3.返回SearchResult对象
        searchResult.setItems(pageResult.getItems());
        searchResult.setTotal(pageResult.getTotal());
        searchResult.setTotalPage(pageResult.getTotalPage());
        searchResult.setFilterConditions(filterConditions);
        return searchResult;
    }

    /**
     * 统一抽取了查询条件（商品列表和聚合查询共用的条件）
     * @param searchRequest
     * @return
     */
    private NativeSearchQueryBuilder createNativeQueryBuilder(SearchRequest searchRequest) {
        //1.创建本地查询构造器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //2.往本地查询构造器添加条件
        //1）创建布尔组合查询构造器
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //2）往布尔组合查询构造器添加条件

        //高亮步骤1：把spuName作为搜索条件
        boolQueryBuilder.must(QueryBuilders.multiMatchQuery(searchRequest.getKey(),"all","spuName"));

        //给布尔组合添加过滤条件
        addFilterParams(searchRequest, boolQueryBuilder);

        //3）把布尔组合构造器 添加到本地查询构造器
        queryBuilder.withQuery(boolQueryBuilder);

        return queryBuilder;
    }

    /**
     * 在本地查询构造器中添加过滤条件
     * @param searchRequest
     * @param boolQueryBuilder
     */
    public void addFilterParams(SearchRequest searchRequest, BoolQueryBuilder boolQueryBuilder) {
        //取出页面传递的过滤条件
        Map<String, Object> filterParams = searchRequest.getFilterParams();
        if(filterParams!=null){
            filterParams.entrySet().forEach(entry->{
                String key = entry.getKey();
                Object value = entry.getValue();

                //如果是分类和品牌，单独处理 key
                if(key.equals("分类")){
                    key = "categoryId";
                }
                else if(key.equals("品牌")){
                    key = "brandId";
                }else {
                    key = "specs." + key + ".keyword";
                }

                boolQueryBuilder.filter(QueryBuilders.termQuery(key,value));
            });
        }
    }

    /**
     * 查询商品搜索过滤条件
     * @param searchRequest
     * @return
     */
    public Map<String,Object> filterConditionsQuery(SearchRequest searchRequest) {
        //定义一个Map集合，用于存储所有过滤条件(注意： 页面展示的元素是有序的)
        Map<String,Object> filterConditions = new LinkedHashMap<>();

        NativeSearchQueryBuilder queryBuilder = createNativeQueryBuilder(searchRequest);

        //设置分页参数 (注意：SpringDataElasticsearch的每页大小不允许为0)
        //queryBuilder.withPageable(PageRequest.of(0,0));

        //去除搜索列表结果，只保留聚合结果（结果优化）
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""},null));


        //定义分类聚合和品牌聚合别名
        String categoryAgg = "categoryAgg";
        String brandAgg = "brandAgg";

        //1）设置分类聚合条件
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAgg).field("categoryId"));
        //2）设置品牌聚合条件
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAgg).field("brandId"));

        //3）执行聚合查询
        //AggregatedPage: 必须使用AggregatedPage封装聚合结果
        AggregatedPage<Goods> aggregatedPage = esTemplate.queryForPage(queryBuilder.build(),Goods.class);

        //4）获取所有聚合结果
        Aggregations aggregations = aggregatedPage.getAggregations();

        //5）根据分类聚合别名取出分类聚合结果
        Terms categoryTerms = aggregations.get(categoryAgg);

        //6）取出分类聚合内容
        List<Long> categoryIds = categoryTerms
                                    .getBuckets()
                                    .stream()
                                    .map(Terms.Bucket::getKeyAsNumber)
                                    .map(Number::longValue) //把Number类型转换为long类型
                                    .collect(Collectors.toList());

        //7）根据分类Id集合 查询 分类对象集合
        List<Category> categoryList = itemClient.findCategoriesByIds(categoryIds);

        //8）把分类数据存入Map集合
        filterConditions.put("分类",categoryList);

        //9）根据品牌聚合别名取出品牌聚合结果
        Terms brandTerms = aggregations.get(brandAgg);

        //10）取出品牌聚合内容
        List<Long> brandIds = brandTerms
                .getBuckets()
                .stream()
                .map(Terms.Bucket::getKeyAsNumber)
                .map(Number::longValue) //把Number类型转换为long类型
                .collect(Collectors.toList());

        //11）根据品牌Id集合 查询 品牌对象集合
        List<Brand> brandList = itemClient.findBrandsByIds(brandIds);

        //12）把分类数据存入Map集合
        filterConditions.put("品牌",brandList);


        //=====动态过滤条件查询=====
        if(categoryIds!=null){
            categoryIds.forEach(cid->{
                //1）根据分类查询用于搜索的规格参数
                List<SpecParam> specParams = itemClient.findSpecParams(null, cid, true);

                //2）使用规格参数，作为聚合条件添加到本地查询构造器
                specParams.forEach(specParam -> {
                    queryBuilder.addAggregation(AggregationBuilders.terms(specParam.getName()).field("specs."+specParam.getName()+".keyword"));
                });

                //3）执行聚合查询
                AggregatedPage<Goods> specAggregatedPage = esTemplate.queryForPage(queryBuilder.build(),Goods.class);

                Aggregations specAggregations = specAggregatedPage.getAggregations();

                //4）取出所有规格参数的聚合结果
                specParams.forEach(specParam -> {
                    //取出每个规格参数的聚合结果
                    Terms specTerms = specAggregations.get(specParam.getName());

                    //从聚合结果中取出key
                    List<String> specKeys = specTerms
                                             .getBuckets()
                                             .stream()
                                             .map(Terms.Bucket::getKeyAsString)
                                             .collect(Collectors.toList());


                    //5)将结果存入filterConditions集合中
                    filterConditions.put(specParam.getName(),specKeys);
                });


            });
        }


        return filterConditions;
    }

    /**
     * 查询商品分页列表结果
     * @param searchRequest
     * @return
     */
    public PageResult<GoodsDTO> itemQueryPage(SearchRequest searchRequest) {
        NativeSearchQueryBuilder queryBuilder = createNativeQueryBuilder(searchRequest);

        //4）添加结果过滤
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","spuName","subTitle","skus"},null));

        //5）添加分页条件（参数一是当前页码，从0开始，参数二：每页大小）
        queryBuilder.withPageable(PageRequest.of(searchRequest.getPage()-1,searchRequest.getSize()));

        //高亮步骤2：把spuName作为高亮字段，添加高亮格式
        HighlightUtils.highlightField(queryBuilder,"spuName");

        //添加排序字段
        addOrderFields(searchRequest,queryBuilder);

        //3.执行查询，获取结果
        //高亮步骤3：创建SearchResultMapper的实现类，重写获取高亮内容，把高亮内容设置到Goods的spuName属性中
        Page<Goods> pageBean =  esTemplate.queryForPage(queryBuilder.build(),Goods.class,HighlightUtils.highlightBody(Goods.class,"spuName"));

        //4.处理结果
        List<Goods> goodsList = pageBean.getContent();
        //把Goods的集合转换为GoodsDTO集合
        List<GoodsDTO> goodsDTOList = BeanHelper.copyWithCollection(goodsList, GoodsDTO.class);
        //打包PageResult对象
        PageResult<GoodsDTO> pageResult = new PageResult<>(
                pageBean.getTotalElements(),
                Long.valueOf(pageBean.getTotalPages()),
                goodsDTOList);

        //5.返回结果
        return pageResult;
    }


    public List<GoodsDTO> goodsPageChange(SearchRequest searchRequest) {
        PageResult<GoodsDTO> pageResult = this.itemQueryPage(searchRequest);
        return pageResult.getItems();
    }

    public void indexCreate(Long spuId) {
        try {
            //1.根据spuId查询SpuDTO
            SputDTO sputDTO = itemClient.findSpuById(spuId);
            //2.把SpuDTO转换为Goods
            Goods goods = this.buildGoods(sputDTO);
            //3.保存Goods到索引库
            searchRepository.save(goods);
        } catch (Exception e) {
            e.printStackTrace();
            throw new LyException(ExceptionEnum.INSERT_OPERATION_FAIL);
        }
    }

    public void indexDelete(Long spuId) {
        searchRepository.deleteById(spuId);
    }

    /**
     * 在请求参数中取出排序的字段，通过SortBuilders构建排序规则，作为后续查询的的query
     * -- Fields-> Map<String,Integer>
     *     - 0: ASC 升序
     *     - 1: DESC 降序
     * @author Dereck
     * @param searchRequest 请求参数集合
     * @param queryBuilder
     */
    private void addOrderFields(SearchRequest searchRequest, NativeSearchQueryBuilder queryBuilder) {

        Map<String, Integer> fields = searchRequest.getSortFields();

        if(fields!=null && fields.size()!=0){
            //取出全部的字段
            Set<Map.Entry<String, Integer>> entries = fields.entrySet();
            //遍历添加排序条件
            for (Map.Entry<String, Integer> entry : entries) {

                SortBuilder sortBuilder = null;
                if(entry.getValue()==0){
                    //
                    sortBuilder = SortBuilders.fieldSort(entry.getKey()).order(SortOrder.ASC);
                }else {
                    //
                    sortBuilder = SortBuilders.fieldSort(entry.getKey()).order(SortOrder.DESC);
                }

                //添加排序字段，进行查询
                queryBuilder.withSort(sortBuilder);
            }
        }
    }
}
